import types
import sys
from functools import partial

# Import django --- your environment settings must be right. This hack creates 
# some random django environment for testing purposes if the module is called
# as a script.
if __name__ == '__main__':
    from tutor.db import settings
    from django.core.management import setup_environ
    setup_environ(settings, 'tutor.db.settings')
from django.db import models
from fields import *

# Import schemas
from chips.jsonlib.schemas import *
from chips.jsonlib.schemas.ext.datetime import *

class ToDjango(object):
    class json_property(property):
        pass

    PREFIX = 'django'
    TYPES = {
        Text: models.TextField,
        Bool: models.BooleanField,
        Int: models.IntegerField,
        Real: models.FloatField,
        Email: models.EmailField,
        JSONTime: models.TimeField,
        JSONDate: models.DateField,
        JSONDateTime: models.DateTimeField,
    }

    ATTRS = {
        'default': 'default',
        'desc': 'help_text',
        'label': 'verbose_name',
        'is_unique': 'unique',
        'is_ref': 'primary_key'
    }

    def __init__(self, cls=None, schema=None, name=None, db_fields=None):
        if cls is None:
            class cls(object):
                pass
        if schema is None:
            schema = cls.schema
        if name is None:
            name = schema.name
        if not name:
            raise ValueError('empty name')
        if db_fields is None:
            db_fields = schema.keys()
        self.cls = cls
        self.name = name
        self.schema = schema
        self.db_fields = set(db_fields)
        self.db_fields_init = getattr(cls, 'db_fields_init', {})

        # Start _fields
        self._fields = {}
        if len(db_fields) < len(schema):
            self._fields['_json'] = JSONField(default={})

        # Obtain information from schema
        for k, v in self.schema.items():
            if k in db_fields:
                vname = type(v).__name__
                try:
                    conv = getattr(self, 'convert_' + vname)
                except AttributeError:
                    conv = self.convert_generic
                conv(k, v)
            else:
                try:
                    T_json = getattr(self.cls, 'T_%s_json' % k).im_func
                except AttributeError:
                    T_json = None
                try:
                    T_object = getattr(self.cls, 'T_%s_object' % k).im_func
                except AttributeError:
                    T_object = None

                prop = self.json_key_property(k, T_json, T_object)
                self._fields[k] = prop

    #===========================================================================
    #                             Type Conversors
    #===========================================================================
    def convert_generic(self, key, vobj, new_type=None, args=None, kwds=None):
        if new_type is None:
            try:
                new_type = self.TYPES[type(vobj)]
            except KeyError:
                raise TypeError('unuportted type, %s' % type(vobj))

        # Build keyword attributes to call the new to_type() object
        kwds = (kwds if kwds is not None else {})
        args = (args if args is not None else ())
        for orig, new in self.ATTRS.items():
            try:
                kwds[new] = getattr(vobj, orig)
            except AttributeError:
                pass

        # Build from metadata
        L = len(self.PREFIX) + 1
        for attr in dir(vobj.root):
            if attr.startswith(self.PREFIX):
                kwds[attr[L:]] = getattr(vobj.root, attr)
        kwds.update(self.db_fields_init.get(key, {}))

        self._fields[key] = new_type(*args, **kwds)

    def convert_Str(self, key, vobj):
        if getattr(vobj.root, 'is_text_field', False):
            self.convert_generic(key, vobj, models.TextField)
        else:
            if vobj.max_length is None:
                self.convert_generic(key, vobj, StrField)
            else:
                self.convert_generic(key, vobj, models.CharField)

    def convert_Ref(self, key, vobj):
        if vobj.type == 'self':
            self.convert_generic(key, vobj, models.ForeignKey, args=('self',))
        else:
            print(vobj.type)
            raise TypeError

    def type(self, name=None, app_label=None, module=None, schema_attr='schema'):
        '''
        Return a Django type from the JSON schema used to initialize the 
        Conversor.
        
        
        Optional Arguments
        ------------------
        
        name : str
            Name of the generated type
            
        app_label : str
            Name of the Django application for which the model is build.
        
        module : str
            Name of the module the type was created. Usually it is 
            app_label.models. 
            
        schema_attr : str
            Name of the class attribute in which the current schema lives in.
            If it is None, the schema is not saved in the newly created type. 
        '''

        dic = {}

        # Class name
        if name is None:
            name = self.name

        # Schema
        if schema_attr:
            dic[schema_attr] = self.schema

        # Set the right module
        if module:
            dic['__module__'] = module
        else:
            if app_label:
                dic['__module__'] = '%s.models' % app_label
            else:
                G = globals()
                dic['__module__'] = G['__package__'] or G['__name__']

        # Assure that module is on sys.modules
        if dic['__module__'] not in sys.modules:
            raise RuntimeError("you must import module '%s' before creating "
                               "this Django type" % dic['__module__'])

        # Set the correct app_label attribute
        if app_label is None:
            label_name = app_label
            class Meta:
                app_label = label_name
            dic['Meta'] = Meta

        # Return django type
        dic.update(self._fields)
        return type(name, (models.Model,), dic)

    def subtype(self):
        '''
        Build module from type 'cls'.  
        
        
        Arguments
        ---------
        
        cls : type
            Class to derive new django type.
            
        is_subclass : bool
            If True, the resulting type will be a subclass of cls. Otherwise it
            will only take the attributes/_fields and methods from cls and build
            a new subclass of django.db.models.Model. 
            
        '''
        cls = self.cls
        if models.Model in cls.mro():
            raise ValueError('cannot decorate django classes.')

        # Sort objects defined in cls
        classdict = dict(cls.__dict__)
        fields = {}
        methods = {}
        for k, v in classdict.items():
            if isinstance(v, models.Field):
                fields[k] = classdict.pop(k)
            elif isinstance(v, types.MethodType):
                methods[k] = classdict.pop(k).im_func

        # Remove conflicting attributes
        classdict.pop('__name__', None)
        classdict.pop('__class__', None)
        classdict.pop('__dict__', None)

        # Fix bases
        bases = cls.mro()[1:]
        bases.insert(0, models.Model)
        if bases[-1] is object:
            del bases[-1]
        bases = tuple(bases)

        # Prepare classdict
        classdict.update(fields)
        classdict.update(methods)
        classdict.update(self._fields)
        classdict = dict((str(k), v) for (k, v) in classdict.items())

        name = cls.__name__
        return type(name, bases, classdict)

    #===========================================================================
    # Useful methods for derived classes
    #===========================================================================
    def json_method(self, schema):
        def json(self, **kwds):
            if kwds.get('raw', False):
                try:
                    return self._json
                except AttributeError:
                    self._json = {}
                    return self._json
            else:
                items = ((k, getattr(self, k, None)) for k in schema)
                return dict((k, v) for (k, v) in items if v != schema[k].default_or_null)
        return json

    def validate_method(self, schema):
        def validate(o_self, **kwds):
            schema.validate(o_self, **kwds)
        return validate

    def fget_method(self, attr, schema, T_func=None):
        def fget(o_self):
            json = getattr(o_self, '_json', {})
            value = json.get(attr, schema.default_or_null)
            if T_func is None:
                return value
            else:
                return T_func(o_self, value)
        return fget

    def fset_method(self, attr, schema, T_func=None):
        def fset(o_self, value):
            if T_func is not None:
                value = T_func(o_self, value)
            o_self._json[attr] = value
        return fset

    def fdel_method(self, attr):
        def fdel(o_self):
            try:
                root = o_self._json
            except AttributeError:
                return
            del root[prop]
        return fdel

    def json_key_property(self, name, T_json=None, T_object=None):
        schema = self.schema[name]
        fget = self.fget_method(name, schema, T_object)
        fset = self.fset_method(name, schema, T_json)
        fdel = self.fdel_method(name)
        return self.json_property(fget, fset, fdel)

if __name__ == '__main__':
    class Test(object):
        schema = Object({   'hello': Str('world!'),
                            'answer': Int(42),
                            'bubu': Str('conossor', desc='fsdfsd')
                        },
                        name='test')
    tc = ToDjango(Test)

    import test
    import test.models as tmodels
    m = tc.type(app_label='test')
#    print '\n'.join(sorted(dir(m)))

    tt = tc.subtype()
#    print '\n'.join(sorted(dir(tt)))

    import doctest
    doctest.testmod(optionflags=doctest.REPORT_ONLY_FIRST_FAILURE, verbose=0)
